using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Windows.Forms;
using System.Xml;
using System.Xml.Serialization;
using WowAceUpdater.Properties;

namespace WowAceUpdater
{
    internal static class WowAceFilesServer
    {
        #region Constants

        private const string FILES_SERVER_DEFAULT = "http://files.wowace.com/";

        private const string INDEX_EXT_FILE_NAME = "latest.xml";

        private const string INDEX_NOEXT_FILE_NAME = "latest-noext.xml";

        private const string MIRROR_FILE_NAME = "mirrors.txt";

        #endregion

        #region Fields

        private static readonly string LOCAL_EXT_IDX_FILE = Path.Combine(FileSystem.TempDirectory, INDEX_EXT_FILE_NAME);

        private static readonly string LOCAL_MIRROR_FILE = Path.Combine(FileSystem.TempDirectory, MIRROR_FILE_NAME);

        private static readonly string LOCAL_NOEXT_IDX_FILE =
            Path.Combine(FileSystem.TempDirectory, INDEX_NOEXT_FILE_NAME);

        private static readonly Uri MIRROR_FILE_HOST = new Uri("http://svn1.wowace.com/");

        private static readonly Uri MIRROR_FILE_URL = new Uri(MIRROR_FILE_HOST + MIRROR_FILE_NAME);

        private static readonly Object oLock = new Object();
        private static bool _bWithExternals = true;

        private static bool _cacheDirty;

        private static SortedDictionary<string, rssAddonItem> _FilesLookup;

        private static XmlDocument _FilesModeIndex;

        private static XmlDocument _FilesWithExtIndex;

        private static XmlDocument _FilesWithoutExtIndex;

        private static string[] _MirrorList = new[] {FILES_SERVER_DEFAULT};

        private static WebClient _mirrorUpdateWC;

        private static SortedDictionary<string, rssAddonItem> _providerList;

        private static SortedDictionary<string, rssAddonItem> _WithExtFilesLookup ;

        private static SortedDictionary<string, rssAddonItem> _WithoutExtFilesLookup ;

        public static int CACHE_MIN_MINUTES = 10;

        #endregion

        #region Properties

        public static TimeSpan CacheAge
        {
            get
            {
                if (File.Exists(LOCAL_EXT_IDX_FILE))
                {
                    DateTime dt = File.GetLastWriteTimeUtc(LOCAL_EXT_IDX_FILE);
                    TimeSpan age = DateTime.UtcNow - dt;
                    return age;
                }
                
                
                return TimeSpan.MaxValue;
            }         
        }

        public static bool CacheDirty
        {
            get
            {
                lock (oLock)
                {
                    CheckIndexCache();
                    return (_cacheDirty || CacheEmpty);
                }
            }
            set
            {
                lock (oLock)
                {
                    _cacheDirty = value;
                    if (_cacheDirty)
                    {
                        CacheEmpty = true;
                    }
                }
            }
        }

        public static bool CacheEmpty
        {
            get
            {
                lock (oLock)
                {
                    return (!File.Exists(LOCAL_EXT_IDX_FILE) ||
                            !File.Exists(LOCAL_NOEXT_IDX_FILE));
                }
            }
            set
            {
                if (value)
                {
                    lock (oLock)
                    {
                        try
                        {
                            File.Delete(LOCAL_EXT_IDX_FILE);
                            File.Delete(LOCAL_NOEXT_IDX_FILE);
                        }
                        catch (Exception e)
                        {
                            Program.LogDbg(e.ToString());
                        }

                        _FilesLookup = null;
                        _WithExtFilesLookup = null;
                        _WithoutExtFilesLookup = null;
                        _FilesWithoutExtIndex = null;
                        _FilesWithExtIndex = null;

                        _FilesModeIndex = null;
                    }
                }
            }
        }

        public static XmlDocument FilesIndex
        {
            get
            {
                lock (oLock)
                {
                    if (CacheEmpty)
                        UpdateIndexFile();

                    if (_FilesModeIndex != null)
                        if (WithExternals)
                            _FilesModeIndex = WithExtFilesIndex;
                        else
                            _FilesModeIndex = WithoutExtFilesIndex;

                    return _FilesModeIndex;
                }
            }
        }


        /// <summary>
        ///  This is called when you don't know the update context
        /// </summary>
        internal static SortedDictionary<string, rssAddonItem> FilesLookup
        {
            get
            {
                lock (oLock)
                {
                    if (_FilesLookup == null)
                        if (WithExternals)
                            _FilesLookup = WithExtFilesLookup;
                        else
                            _FilesLookup = WithoutExtFilesLookup;

                    return _FilesLookup;
                }
            }
        }

        public static string[] MirrorList
        {
            get { return _MirrorList; }
            set { _MirrorList = value; }
        }

        /// <summary>
        /// 
        /// </summary>
        public static SortedDictionary<string, rssAddonItem> ProviderAddon
        {
            get
            {
                if (_providerList == null)
                {
                    _providerList = new SortedDictionary<string, rssAddonItem>();

                    foreach (rssAddonItem addon in FilesLookup.Values)
                    {
                        if (addon.provides != null)
                            foreach (string node in addon.provides)
                            {
                                if (_providerList.ContainsKey(node))
                                    _providerList[node] = addon;
                                else
                                    _providerList.Add(node, addon);
                            }
                    }
                }

                return _providerList;
            }
        }

        public static bool WithExternals
        {
            get { return _bWithExternals; }
            set
            {
                lock (oLock)
                {
                    if (_bWithExternals != value)
                        _FilesLookup = null;
                }

                _bWithExternals = value;
            }
        }

        public static XmlDocument WithExtFilesIndex
        {
            get
            {
                lock (oLock)
                {
                    if (CacheEmpty)
                        UpdateIndexFile();

                    if (_FilesWithExtIndex == null)
                        _FilesWithExtIndex =
                            LoadIndexFromLocalFile(LOCAL_EXT_IDX_FILE);

                    return _FilesWithExtIndex;
                }
            }
        }

        internal static SortedDictionary<string, rssAddonItem> WithExtFilesLookup
        {
            get
            {
                lock (oLock)
                {
                    if (_WithExtFilesLookup == null)
                        _WithExtFilesLookup = LoadIndexFromXML(WithExtFilesIndex);
                    return _WithExtFilesLookup;
                }
            }
        }

        public static XmlDocument WithoutExtFilesIndex
        {
            get
            {
                lock (oLock)
                {
                    if (CacheEmpty)
                        UpdateIndexFile();

                    if (_FilesWithoutExtIndex == null)
                        _FilesWithoutExtIndex =
                            LoadIndexFromLocalFile(LOCAL_NOEXT_IDX_FILE);

                    return _FilesWithoutExtIndex;
                }
            }
        }

        internal static SortedDictionary<string, rssAddonItem> WithoutExtFilesLookup
        {
            get
            {
                lock (oLock)
                {
                    if (_WithoutExtFilesLookup == null)
                        _WithoutExtFilesLookup = LoadIndexFromXML(WithoutExtFilesIndex);
                    return _WithoutExtFilesLookup;
                }
            }
        }

        internal static void ForceServerCacheMiss(Uri url)
        {
            using (WebClient wc = GetWebConfiguredWebClient())
            {
                wc.Headers.Add(HttpRequestHeader.CacheControl, "no-cache");
                Program.LogDbg(wc.DownloadString(url));
            }
        }

        internal class ServerAddonCollection : SortedDictionary<string, rssAddonItem>
        {
        }

        #endregion

        #region Public Methods

        private static readonly List<string> NO_DEPENDANCIES = new List<string>(0);
        private static XmlSerializer _AddonItemSerializer;

        public static List<string> _GetAddonDependancies(string addonName)
        {
            return GetAddonDependancies(FilesLookup[addonName]);
        }

        public static VersionNumber _GetAddonRevision(string addonName)
        {
            try
            {
                return GetAddonRevision(FilesLookup[addonName]);
            }
            catch
            {
                return VersionNumber.NO_VERSION;
            }
        }

        public static void DownloadAddon(string addonName, string localZipPath)
        {
            DownloadFile(GetZipFileUrl(addonName), localZipPath);
        }

        public static void DownloadAddon(rssAddonItem addonSource, string localZipPath)
        {
            DownloadFile(GetZipFileUrl(addonSource), localZipPath);
        }

        public static void DownloadFile(string fileUrl, string localFileName)
        {
            long len = 0;
            using (WebClient wc = GetWebConfiguredWebClient())
            {
                bool incompleteFile;

                do
                {
                    incompleteFile = false;

                    if (File.Exists(localFileName))
                        File.Delete(localFileName);

                    wc.DownloadFile(fileUrl, localFileName);
                    WebHeaderCollection whc = wc.ResponseHeaders;

                    Program.LogDbg(whc.ToString());
                    string header = whc[HttpResponseHeader.ContentLength];


                    if (!string.IsNullOrEmpty(header))
                    {
                        Int64.TryParse(header, out len);

                        FileInfo file = new FileInfo(localFileName);
                        Program.LogDbg(
                            string.Format("Downloaded {0} size {1} filesize {2}",
                                          localFileName,
                                          len,
                                          file.Length));

                        if (file.Length < len)
                            incompleteFile = true;
                    }
                } while (incompleteFile);
            }

            if (len > 0)
                MainForm.AddBandwidthUsed(len);
        }

        public static List<string> GetAddonDependancies(rssAddonItem addonSource)
        {
            if (addonSource.dependencies == null)
                return NO_DEPENDANCIES;

            List<string> deps = new List<string>();

            foreach (string d in addonSource.dependencies)
                if (FilesLookup.ContainsKey(d))
                    deps.Add(d);
                else
                {
                    if (ProviderAddon.ContainsKey(d))
                    {
                        string p = ProviderAddon[d].title;
                        deps.Add(p);

                        Program.LogDbg("Found " + d + " in " + p);
                    }
                }
            return deps;
        }

        public static VersionNumber GetAddonRevision(XmlNode xmlAddon)
        {
            VersionNumber.ExternalsType ext = VersionNumber.ExternalsType.UNKNOWN;
            VersionNumber.StabilityType stab = VersionNumber.StabilityType.UNKNOWN;

            string sguid = null;
            string ver = null;
            string sstab = null;

            foreach (XmlNode node in xmlAddon.ChildNodes)
                if (node.Name == "guid")
                {
                    sguid = node.FirstChild.Value;
                }
                else if (node.Name == "wowaddon:version")
                {
                    ver = node.FirstChild.Value;
                }
                else if (node.Name == "wowaddon:stable")
                {
                    sstab = node.FirstChild.Value;
                }

            if (sguid != null)
            {
                ext = sguid.ToLowerInvariant().Contains("no-ext")
                          ? VersionNumber.ExternalsType.WITHOUT
                          : VersionNumber.ExternalsType.WITH;
            }

            if (! string.IsNullOrEmpty(sstab))
            {
                stab = (sstab.ToLowerInvariant().Contains("false"))
                           ? VersionNumber.StabilityType.UNSTABLE
                           : VersionNumber.StabilityType.STABLE;
            }

            if (ver != null)
                return VersionNumber.Parse(ver, ext, stab);

            return VersionNumber.NO_VERSION;
        }

        public static VersionNumber GetAddonRevision(rssAddonItem xmlAddon)
        {
            VersionNumber.ExternalsType ext = VersionNumber.ExternalsType.UNKNOWN;
            VersionNumber.StabilityType stab;

            if (!string.IsNullOrEmpty(xmlAddon.guid))
                if (xmlAddon.guid.ToLowerInvariant().Contains("no-ext"))
                    ext = VersionNumber.ExternalsType.WITHOUT;
                else
                    ext = VersionNumber.ExternalsType.WITH;

            stab = xmlAddon.stable ? VersionNumber.StabilityType.STABLE : VersionNumber.StabilityType.UNSTABLE;


            if (!string.IsNullOrEmpty(xmlAddon.version))
                return VersionNumber.Parse(xmlAddon.version, ext, stab);

            return VersionNumber.NO_VERSION;
        }

        public static string GetComments(rssAddonItem addonSource)
        {
            return addonSource.comments;
        }

        public static rssAddonItem GetCurrentAddonSource(string addonName, VersionNumber localRev)
        {
            rssAddonItem addonSource;
            if (localRev.Externals == VersionNumber.ExternalsType.WITH)
                addonSource = WithExtFilesLookup[addonName];
            else if (localRev.Externals == VersionNumber.ExternalsType.WITHOUT)
                addonSource = WithoutExtFilesLookup[addonName];
            else
                addonSource = FilesLookup[addonName];
            return addonSource;
        }

        public static string GetLink(rssAddonItem addonSource)
        {
            return addonSource.link;
        }

        public static void GetMirrors()
        {
            if (_MirrorList.Length == 1 && File.Exists(LOCAL_MIRROR_FILE))
            {
                _MirrorList = File.ReadAllLines(LOCAL_MIRROR_FILE);

                DateTime dt = File.GetLastWriteTimeUtc(LOCAL_MIRROR_FILE);
                TimeSpan age = DateTime.UtcNow - dt;

                Program.LogDbg("Cached Mirror List is " + age.TotalHours + " hours old");
                if (age.TotalDays > 5 || _MirrorList.Length == 0)
                    File.Delete(LOCAL_MIRROR_FILE);
            }
            else
            {
                if (_mirrorUpdateWC == null)
                {
                    FileSystem.EnsureDirExists(FileSystem.TempDirectory);
                    _mirrorUpdateWC = GetWebConfiguredWebClient();
                    _mirrorUpdateWC.DownloadDataCompleted += MirrorsUpdated;
                    _mirrorUpdateWC.DownloadFileAsync(MIRROR_FILE_URL, LOCAL_MIRROR_FILE);
                }
            }
        }

        public static rssAddonItem GetItemFromSource(XmlNode addonSource)
        {
            rssAddonItem r = null;
            try
            {
                if (_AddonItemSerializer == null)
                    _AddonItemSerializer = new XmlSerializer(typeof (rssAddonItem));
                r = (rssAddonItem) _AddonItemSerializer.Deserialize(new StringReader(addonSource.OuterXml));
            }
            catch (Exception e)
            {
                Program.LogErr(e.ToString());
            }
            return r;
        }

        public static string GetNameFromSource(rssAddonItem addonSource)
        {
            return addonSource.title;
        }

        public static string GetNameFromSource(XmlNode addonSource)
        {
            string title = null;

            foreach (XmlNode node in addonSource.ChildNodes)
                if (node.Name == "title")
                {
                    title = node.FirstChild.Value;
                }

            return title;
        }

        public static string GetZipFileName(string addonName, VersionNumber revision)
        {
            return String.Format("{0}-r{1}.zip", addonName, revision);
        }

        public static string GetZipFileUrl(string addonName)
        {
            return GetZipFileUrl(FilesLookup[addonName]);
        }

        public static string GetZipFileUrl(rssAddonItem addonSource)
        {
            return addonSource.enclosure.url;
        }

        public static void UpdateIndexFile(bool forceDownload)
        {
            lock (oLock)
            {
                try
                {
                    _UpdateIndexFile(forceDownload);
                }
                catch (Exception e)
                {
                    Program.LogDbg(e.ToString());
                }
            }
        }

        // We call UpdateIndexFile(true) when they choose Reload Index
        // Also, we call UpdateIndexFile() when they download

        public static void UpdateIndexFile()
        {
            UpdateIndexFile(false);
        }

        #endregion

        #region Private Methods

        private static void _UpdateIndexFile(bool forceDownload)
        {
            if (forceDownload || CacheDirty)
            {
                using (WebClient wc = GetWebConfiguredWebClient())
                {
                    using (WebClient wc2 = GetWebConfiguredWebClient())
                    {
                        Program.LogDbg("Downloading New Indexes");

                        wc.Headers.Add("Accept-Encoding:  gzip");
                        wc2.Headers.Add("Accept-Encoding:  gzip");

                        FileSystem.EnsureDirExists(FileSystem.TempDirectory);

                        CacheDirty = true;
                        string address = String.Format("{0}/{1}", Settings.Default.AceFilesPath, INDEX_EXT_FILE_NAME);

                        //                       wc.DownloadProgressChanged += MainForm.wc_DownloadProgressChanged;
                        wc.DownloadFileCompleted += MainForm.wc_DownloadFileCompleted;

                        wc.DownloadFileAsync(new Uri(address), LOCAL_EXT_IDX_FILE);

                        address = String.Format("{0}/{1}", Settings.Default.AceFilesPath, INDEX_NOEXT_FILE_NAME);

//                        wc2.DownloadProgressChanged += MainForm.wc2_DownloadProgressChanged;
                        wc2.DownloadFile(new Uri(address), LOCAL_NOEXT_IDX_FILE);
                        WebHeaderCollection whc = wc2.ResponseHeaders;
                        string header = whc[HttpResponseHeader.ContentLength];
                        if (!string.IsNullOrEmpty(header))
                        {
                            long len;
                            Int64.TryParse(header, out len);

                            if (len > 0)
                                MainForm.AddBandwidthUsed(len);
                        }

                        //                      wc2.DownloadFileCompleted += MainForm.wc2_DownloadFileCompleted;
                        while (wc.IsBusy || wc2.IsBusy)
                        {
                            Thread.Sleep(250);
                            Application.DoEvents();
                        }
                    }
                }

                CacheDirty = false;
            }
        }

        private static void CheckIndexCache()
        {
            TimeSpan age = CacheAge;

            Program.LogDbg("Cached Index is " + age.TotalMinutes + " minutes old");
            if (age.TotalMinutes > CACHE_MIN_MINUTES)
                _cacheDirty = true;
        }

        private static WebClient GetWebConfiguredWebClient()
        {
            WebClient result = new WebClient();
            result.Headers.Add("User-Agent",
                               String.Format("{0}/{1}", Application.ProductName, Application.ProductVersion));
            return result;
        }

        private static XmlDocument LoadIndexFromLocalFile(string fname)
        {
            lock (oLock)
            {
                XmlDocument x = new XmlDocument();

                try
                {
                    using (FileStream fs = new FileStream(fname, FileMode.Open))
                    {
                        using (Stream gzi = Compression.OpenGZipInputStream(fs))
                            x.Load(gzi);
                    }

                    return x;
                }
                catch (Exception)
                {
                    try
                    {
                        using (FileStream fs = new FileStream(fname, FileMode.Open))
                            x.Load(fs);

                        return x;
                    }
                    catch (Exception)
                    {
                        CacheEmpty = true;
                        x = null;
                    }
                }

                return x;
            }
        }

        private static SortedDictionary<string, rssAddonItem> LoadIndexFromXML(XmlDocument x)
        {
            if (x == null)
                return null;

            Program.LogDbg(string.Format("Rebuilding Lookup {0}",
                                         x == _FilesWithExtIndex ? "WithExt" : "NoExt"));

            XmlNodeList n = x.GetElementsByTagName("item");

            SortedDictionary<string, rssAddonItem> fl = new SortedDictionary<string, rssAddonItem>();

            foreach (XmlNode addon in n)
            {
                rssAddonItem item = GetItemFromSource(addon);
                fl.Add(item.title, item);
            }
            return fl;
        }

        private static void MirrorsUpdated(Object sender, DownloadDataCompletedEventArgs e)
        {
            _mirrorUpdateWC = null;
            _MirrorList = File.ReadAllLines(LOCAL_MIRROR_FILE);
        }

        #endregion
    }
}